嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第五天!
昨天我們學習了所有權這個核心概念,但你可能已經發現了一個問題:如果每次把變數傳給函式就會轉移所有權,那程式碼會變得非常難寫和不實用。想像一下,如果你想計算一個字串的長度,卻因此失去了這個字串的使用權,這根本不合理對吧?
幸好,Rust 提供了一個優雅的解決方案:參考 (References) 與借用 (Borrowing)。
今天我們要學習如何在不轉移所有權的情況下,讓函式能夠使用資料。這就像是把你的書「借」給朋友看,而不是直接「送」給他一樣 —— 你依然是書的主人,朋友只是暫時使用而已。
參考就像是變數的「別名」或「指向」,它讓我們可以使用某個值,但不擁有它。在 Rust 中,我們使用 &
符號來建立參考:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 傳遞 s1 的參考
println!("'{}' 的長度是 {}", s1, len); // s1 仍然可以使用!
}
fn calculate_length(s: &String) -> usize { // s 是 String 的參考
s.len()
} // s 離開作用域,但因為它沒有擁有資料,所以不會釋放記憶體
在這個例子中,&s1
建立了一個指向 s1
的參考,而函式參數 s: &String
接收這個參考。重要的是,s1
的所有權沒有轉移,所以我們之後還能繼續使用它!
我們將使用參考稱為「借用」(borrowing),因為我們只是借用值而不擁有它。但借用有一些重要的規則:
fn main() {
let reference_to_nothing = dangle(); // 這會編譯錯誤!
}
fn dangle() -> &String { // 試圖回傳一個參考
let s = String::from("hello");
&s // 回傳 s 的參考,但 s 即將被釋放!
} // s 離開作用域並被釋放,所以參考指向了無效的記憶體
Rust 編譯器會阻止這種「懸置參考」(dangling references) 的產生。
在任何給定時間,你可以擁有:
但不能同時擁有兩者!
讓我們看看為什麼需要這個規則:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 沒問題 - 不可變參考
let r2 = &s; // 沒問題 - 可以有多個不可變參考
println!("{} and {}", r1, r2); // r1 和 r2 在這裡結束使用
let r3 = &mut s; // 沒問題 - 可變參考
println!("{}", r3);
}
但如果同時存在可變和不可變參考就會出錯:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可變參考
let r2 = &mut s; // 編譯錯誤!不能在不可變參考存在時建立可變參考
println!("{} {}", r1, r2);
}
如果我們想要透過參考來修改值,就需要使用可變參考:
fn main() {
let mut s = String::from("hello");
change(&mut s); // 傳遞可變參考
println!("{}", s); // 輸出:hello, world
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
注意幾個重要的點:
mut
&mut
&mut String
同一時間只能有一個可變參考:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 編譯錯誤!
println!("{}, {}", r1, r2);
}
這個限制防止了「資料競爭」(data races),確保在修改資料時不會有其他代碼同時讀取或修改相同的記憶體。
參考的生命週期必須在被參考值的生命週期內:
fn main() {
let r; // 宣告 r,但還沒初始化
{
let x = 5;
r = &x; // 錯誤!x 的生命週期太短
} // x 在這裡被釋放
println!("r: {}", r); // r 參考了已被釋放的記憶體
}
正確的做法:
fn main() {
let x = 5; // x 進入作用域
let r = &x; // r 參考 x
println!("r: {}", r); // 沒問題,x 仍然有效
} // x 和 r 都離開作用域
除了參考整個 String
,我們還可以參考字串的一部分,這叫做「字串切片」:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 或 &s[..5]
let world = &s[6..11]; // 或 &s[6..]
let whole = &s[..]; // 整個字串的切片
println!("hello: {}", hello); // hello
println!("world: {}", world); // world
println!("whole: {}", whole); // hello world
}
字串切片的型別是 &str
,它是一個不可變參考。
fn main() {
let s = "Hello, world!"; // s 的型別是 &str
// 字串字面值是程式二進位檔中特定位置的切片
// 這也是為什麼字串字面值是不可變的
}
使用字串切片可以讓函式更靈活:
fn first_word(s: &String) -> &str { // 只能接受 String 的參考
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn first_word_better(s: &str) -> &str { // 可以接受 &String 和 &str
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
s
}
fn main() {
let my_string = String::from("hello world");
let word1 = first_word(&my_string); // 只能這樣
let word2 = first_word_better(&my_string); // 可以這樣
let word3 = first_word_better("hello"); // 也可以這樣
println!("{}, {}, {}", word1, word2, word3);
}
切片不僅適用於字串,也適用於其他集合:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 型別是 &[i32]
println!("切片:{:?}", slice); // [2, 3]
// 將切片傳給函式
print_slice(slice);
print_slice(&a[..]); // 傳遞整個陣列的切片
}
fn print_slice(slice: &[i32]) {
for item in slice {
println!("值:{}", item);
}
}
// ❌ 錯誤的寫法
fn wrong_way() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // 錯誤!
println!("{} {}", r1, r2);
}
// ✅ 正確的寫法
fn right_way() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 的最後使用
let r2 = &mut s; // 沒問題,r1 已經不再使用
println!("{}", r2);
}
// ❌ 錯誤的寫法
fn wrong_return() -> &String {
let s = String::from("hello");
&s // 錯誤!s 即將被釋放
}
// ✅ 正確的寫法
fn right_return() -> String {
let s = String::from("hello");
s // 回傳所有權
}
// ✅ 或者接受參考參數
fn process_and_return(input: &str) -> &str {
// 處理 input 並回傳它的一部分
&input[0..1]
}
今天我們學會了參考與借用的核心概念:
參考的基本概念:
&
建立參考,&mut
建立可變參考借用的黃金規則:
實用技巧:
&str
比 &String
更靈活&[T]
可以處理陣列的一部分為什麼這樣設計?
寫一個程式實作以下功能:
get_length(s: &String) -> usize
,回傳字串的長度capitalize_first_letter(s: &mut String)
,將字串的第一個字母改為大寫count_words(text: &str) -> usize
,計算字串中的單字數量main
函式中測試這些函式fn main() {
// 測試 get_length
let text = String::from("Hello, world!");
let length = get_length(&text);
println!("字串長度:{}", length);
// 測試 capitalize_first_letter
let mut greeting = String::from("hello, rust!");
println!("修改前:{}", greeting);
capitalize_first_letter(&mut greeting);
println!("修改後:{}", greeting);
// 測試 count_words
let sentence = "Rust is awesome and powerful";
let word_count = count_words(sentence);
println!("單字數量:{}", word_count);
}
fn get_length(s: &String) -> usize {
// 你來實作!
}
fn capitalize_first_letter(s: &mut String) {
// 你來實作!
}
fn count_words(text: &str) -> usize {
// 你來實作!
}
學習重點:
&String
, &str
)&mut String
)注意這個函式回傳 String
而不是 &str
,因為我們需要建立一個新的字串。在後續學習生命週期的概念之後,我們會學到如何更優雅地處理這類問題。
明天我們將學習結構體與列舉,這些自訂型別將讓我們能夠建立更複雜、更有意義的資料結構。我們也會看到參考在結構體中的應用!
參考與借用是 Rust 中非常重要的概念,它們讓我們能夠寫出高效且安全的程式碼。一旦掌握了這些概念,你會發現 Rust 程式設計的美妙之處!
那麼!我們明天見!